/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.core.players;

import com.chasekernan.hxnova.core.components.Component;
import com.chasekernan.hxnova.core.components.ComponentSet;
import com.chasekernan.hxnova.core.components.Defense;
import com.chasekernan.hxnova.core.components.StarScanner;
import com.chasekernan.hxnova.core.dataholders.MessageHandler;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.races.Race;
import com.chasekernan.hxnova.core.races.ResearchCosts;
import com.chasekernan.hxnova.core.races.TechLevel;
import com.chasekernan.hxnova.core.stars.Star;
import com.chasekernan.hxnova.utils.Utils;
import com.chasekernan.hxnova.core.designs.HullDesign;
import com.chasekernan.hxnova.core.battles.BattlePlan;
import com.chasekernan.hxnova.core.dataholders.Global;

/**
    Each game has a set of players that each have a race, techlevel, hw, etc.
    Note: Recognized by id.
**/
class Player {
    
    public static var MAX_DESIGNS : Int = 16;
    
    //ID will be -1 if there is no owner (of a planet, fleet, etc...)
    public var id : Null<Int>;
    
    public var name : String;
    public var race : Race;
    public var techLevel : TechLevel;
    public var designs : Array<HullDesign>;
    public var homeworld : Star;
    public var currentResearch : Hash<Int>;
    public var relations : PlayerRelations;
    public var starScanner : StarScanner;
    public var defenseType : Defense;
    
    public var defaultBattlePlan : BattlePlan;
    public var battlePlans : Array<BattlePlan>;
    
    //percent is the percent of all resources allocated to research.
    //0 = 0%, 1 = 100%
    public var researchField : { field : String, percent : Float };
    
    public function new() {}
    
    /**
        To be called at game creation, this creates the battle plans array and
        the default battle plan for the player.
    **/
    public function setupBattlePlans() {
        defaultBattlePlan = BattlePlan.DEFAULT_BATTLE_PLAN.clone();
        battlePlans = new Array();
    }
    
    /**
        Returns the battle plan with the given name. If no such battle plan 
        exists, then it returns null.
    **/
    public function getBattlePlanByName(name : String) : BattlePlan {
        if (name == "Default") return defaultBattlePlan;
        if (battlePlans == null) return null;
        
        for (bp in battlePlans) {
            if (bp.name == name) return bp;
        }
        
        return null;
    }
    
    /**
        Returns the total cost of a hull design, with racial modifications.
    **/
    public function getCostForDesign(h : HullDesign) : 
            { resources : Int, minerals : MineralSet } {
        //TODO: NEED TO IMPLEMENT
        var cost = h.getTotalCost();
        return { resources : cost.resourceCost, minerals : cost.mineralCost };
    }
    
    /**
        Changes the current research field to which research is applied.
        Will throw an error if [percent] is < 0.0 or > 1.0, or [field] is not a 
        valid tech field.
    **/
    public function changeResearchField(field : String, percent : Float) {
        if (percent < 0 || percent > 1.0) 
            throw "Research percent is out of bounds.";
        if (!ResearchCosts.checkType(field)) 
            throw "No such tech field: " + field;
            
        researchField = { field : field, percent : percent };
    }
    
    /**
        Adds an amount of resources spent on research to a specific field.
        If the amount is enough to reach the next level, a message is sent out 
        and the [techLevel] is updated appropriately.
        Will throw an error if the amount is <= 0 or if there is no such tech
        field.
    **/
    public function addResearch(field : String, amount : Int) {
        if (amount <= 0) throw "Amount must be greater than 0.";
        
        if (!ResearchCosts.checkType(field)) 
            throw "No such tech field: " + field;
        
        if (currentResearch == null) setupResearch();
        
        currentResearch.set(field, currentResearch.get(field) + amount);
        var currentAmount = currentResearch.get(field);
        
        while (true) {
            var cost = techLevel.getCostForIncrease(field, race);
            if (amount >= cost) {
                techLevel.increaseLevel(field);
                amount -= cost;
                Global.messages.sendMessage( { from : Target.Host, 
                        to : Target.PlayerTarget(this), 
                        data : MessageData.TechLevelIncrease(field, 
                        techLevel.getLevel(field)) } );
            } else break;
        }
        
        currentResearch.set(field, 0);
        updateScannerAndDefenseType();
    }
    
    private function setupResearch() {
        currentResearch = new Hash();
        for (f in ResearchCosts.TECH_TYPES) currentResearch.set(f, 0);
    }
    
    /**
        To be called when a tech upgrade is made, this automatically upgrades
        the star scanner and defense type to the best available components.
        NOTE: The scanner tech level is sorted by electronics level, the 
        defense type by energy level.
    **/
    public function updateScannerAndDefenseType() {
        var availableScanners = new Array<StarScanner>();
        for (ss in ComponentSet.getAllComponentsOfType(StarScanner)) {
            if (ss.isAvailableTo(race) && ss.isAvailableToTech(techLevel)) {
                availableScanners.push(cast (ss, StarScanner));
            }
        }
        
        //sort by electronics level if there is at least one scanner.
        if (availableScanners.length != 0) {
            var top = availableScanners[0];
            var topLevel = top.requiredTech.getLevel("electronics");
            
            for (i in 1...availableScanners.length) {
                
                var check = availableScanners[i];
                var level = check.requiredTech.getLevel("electronics");
                
                if(level > topLevel) {
                    top = check;
                    topLevel = level;
                }
            }
            
            starScanner = top;
        }
        
        var availableDefenses = new Array<Defense>();
        for (d in ComponentSet.getAllComponentsOfType(Defense)) {
            if (d.isAvailableTo(race) && d.isAvailableToTech(techLevel)) {
                availableDefenses.push(cast (d, Defense));
            }
        }
        
        //sort by energy level if there is at least one defense type
        if (availableDefenses.length != 0) {
            var top = availableDefenses[0];
            var topLevel = top.requiredTech.getLevel("energy");
            
            for (i in 1...availableDefenses.length) {
                
                var check = availableDefenses[i];
                var level = check.requiredTech.getLevel("energy");
                
                if(level > topLevel) {
                    top = check;
                    topLevel = level;
                }
            }
            
            defenseType = top;
        }
    }
    
    /**
        Returns a player's design based on its name.
        Will throw an error if there is no such design.
    **/
    public function getDesignByName(name : String) : HullDesign {
        for (d in designs) {
            if (d.name == name) return d;
        }
        
        throw "No such design by name " + name;
    }
    
    /**
        Increases the given tech field by 1 (and in consequence, sets the 
        resources spent on that field to 0 and sends a message).
        Will throw an error if there is no such tech field.
        Will do nothing if the maximum level is already achieved.
    **/
    public function incrementTechField(field : String) {
        if (!ResearchCosts.checkType(field)) 
            throw "No such tech field: " + field;
            
        if (currentResearch == null) setupResearch();
        
        //returns -1 if the max level is already achieved.
        var costToNextLevel = techLevel.getCostForIncrease(field, race);
        if (costToNextLevel == -1) return;
        
        var amountNeeded = costToNextLevel - currentResearch.get(field);
        addResearch(field, amountNeeded);
        
        updateScannerAndDefenseType();
    }
    
    /**
        Adds a design to the player's list.
        Will throw an error if this brings the number of designs over the 
        [MAX_DESIGNS], the design is not legal, or there is already a design 
        with this design's name.
        Note: Will do nothing if the design is already in the list.
    **/
    public function addDesign(design : HullDesign) {
        if (!design.isLegal()) throw "Design is not legal.";
        if (designs == null) designs = new Array();
        if ((designs.length + 1) > MAX_DESIGNS) 
            throw "Cannot add any more designs.";
        
        for(d in designs) {
            if(d.isEqualTo(design)) return;
        }
        
        //now check name
        for(d in designs) {
            if(d.name == design.name) {
                throw "There already is a design with name " + d.name;
            }
        }
        
        designs.push(design);
    }
    
    public static var XML_TITLE = "player";
    
    /**
        Writes a player to an xml file.
        Sample xml:
            TODO: Run [writeToXml].
            TODO: Use xml builder here.
    **/
    public static function writeToXml(p : Player) : Xml {
        var xml = Xml.parse('<' + XML_TITLE + '></' + XML_TITLE + '>')
                .firstChild();
                
        if(p.id != null) xml.set("id", Std.string(p.id));
        if(p.name != null) xml.set("name", p.name);
        if (p.researchField != null) {
            xml.set("research_field", p.researchField.field);
            xml.set("research_percent", Std.string(p.researchField.percent));
        }
        
        if (p.starScanner != null) xml.set("scanner", p.starScanner.name);
        if (p.defenseType != null) xml.set("defense", p.defenseType.name);
        
        if (p.relations != null) {
            xml.addChild(PlayerRelations.writeToXml(p.relations));
        }
                
        if(p.designs != null) {
            var designXml = Xml.createElement("designs");
            for(d in p.designs) designXml.addChild(HullDesign.writeToXml(d));
            xml.addChild(designXml);
        }
        
        if (p.techLevel != null) 
            xml.addChild(TechLevel.writeToXml(p.techLevel));
                
        if(p.race != null) xml.addChild(Race.writeToXml(p.race));
        
        if(p.homeworld != null) 
            xml.set("homeworld", Std.string(p.homeworld.id));
            
        if (p.currentResearch != null) {
            var research = Xml.createElement("current_research");
            for (f in p.currentResearch.keys()) {
                var individual = Xml.parse("<" + f + "></" + f + ">")
                        .firstChild();
                individual.firstChild().nodeValue = Std.string(
                        p.currentResearch.get(f));
            }
        }
        
        if (p.defaultBattlePlan != null) {
            var defaultBPXml = Xml.createElement("default_battle_plan");
            defaultBPXml.addChild(BattlePlan.writeToXml(p.defaultBattlePlan));
            xml.addChild(defaultBPXml);
        }
        
        if (p.battlePlans != null) {
            var bpXml = Xml.createElement("battle_plans");
            for(bp in p.battlePlans) bpXml.addChild(BattlePlan.writeToXml(bp));
            xml.addChild(bpXml);
        }
            
                
        return xml;
    }
    
    /**
        Reads a player from an xml file.
        Will throw an error if there are any mistakes.
    **/
    public static function readFromXml(xml : Xml) : Player {
        try {
            var p = new Player();
                        
            if(xml.exists("id")) p.id = Std.parseInt(xml.get("id"));
            if(xml.exists("name")) p.name = xml.get("name");
            if(xml.exists("homeworld")) p.homeworld = Global.stars.getStarById(
                    Std.parseInt(xml.get("homeworld")));
                    
            if (xml.exists("scanner")) {
                p.starScanner = cast ComponentSet.getComponent(
                                xml.get("scanner"), StarScanner);
            }
            
            if (xml.exists("defense")) {
                p.defenseType = cast ComponentSet.getComponent(
                                xml.get("defense"), Defense);
            }
                    
            //if research field exists, then research percent exists
            if (xml.exists("research_field")) {
                p.researchField = { field : xml.get("research_field"), 
                        percent : Std.parseFloat(xml.get("research_percent")) };
            }
            
            if (Utils.xmlHasElement(xml, PlayerRelations.XML_TITLE)) {
                p.relations = PlayerRelations.readFromXml(
                        Utils.getXmlElement(xml, PlayerRelations.XML_TITLE));
            }
                        
            if(Utils.xmlHasElement(xml, "designs")) {
                for (e in Utils.getXmlElement(xml, "designs").elements()) 
                    p.designs.push(HullDesign.readFromXml(e));
            }
            
            if (Utils.xmlHasElement(xml, TechLevel.XML_TITLE)) {
                p.techLevel = TechLevel.readFromXml(
                        Utils.getXmlElement(xml, TechLevel.XML_TITLE));
            }
                        
            if(Utils.xmlHasElement(xml, "race")) {
                p.race = Race.readFromXml(Utils.getXmlElement(xml, "race"));
            }
            
            if (Utils.xmlHasElement(xml, "current_research")) {
                p.currentResearch = new Hash();
                for (e in Utils.getXmlElement(xml, "current_research")) {
                    if (!ResearchCosts.checkType(e.nodeName)) 
                        throw "No such tech field: " + e.nodeName;
                    
                    var amount = Std.parseInt(e.firstChild().nodeValue);
                    if (amount < 0) throw "Amount cannot be less than 0.";
                    
                    p.currentResearch.set(e.nodeName, amount);
                }
            }
            
            if (Utils.xmlHasElement(xml, "default_battle_plan")) {
                p.defaultBattlePlan = BattlePlan.readFromXml(
                        Utils.getXmlElement(xml, "default_battle_plan")
                        .firstElement());
            }
            
            if (Utils.xmlHasElement(xml, "battle_plans")) {
                p.battlePlans = new Array();
                
                var bpXmlElements = Utils.getXmlElement(xml, "battle_plans");
                
                for (bpXml in bpXmlElements.elements()) {
                    p.battlePlans.push(BattlePlan.readFromXml(bpXml));
                }
            }
            
            return p;
        } catch (e : Dynamic) {
            throw "Error while reading player file from xml: " + e;
        }
    }
    
}